MajdiB

Backpressure

Backpressure: The Feature Nobody Asks For Until Everything Falls Over

Every streaming system, every queue, every webhook integration eventually meets a producer faster than its consumer. Backpressure is what decides whether that moment is a graceful slowdown or a cascading outage.

The Assumption Everyone Makes Until It Breaks

Most systems are designed and tested under the assumption that the consumer can keep up. It's a reasonable assumption most of the time — until a downstream service has a slow day, a database index gets dropped during a migration, or a marketing campaign 10x's your event volume overnight. The producer doesn't slow down because it doesn't know anything is wrong. It just keeps sending.

Without backpressure, that gap between "arriving" and "processed" gets absorbed somewhere. Usually that somewhere is an unbounded queue, and an unbounded queue absorbing an unbounded gap is just memory pressure with better branding, right up until the process OOMs and takes down whatever else was consuming from the same broker.

Backpressure Is a Conversation, Not a Wall

The useful mental model isn't "block everything" — it's a feedback signal traveling backward from the slow part of the pipeline to the fast part, so the fast part adjusts instead of finding out the hard way. TCP does this at the transport layer with its receive window; a Node.js stream does it when write() returns false; Kafka consumers do it implicitly by choosing how fast they poll. In every case, the shape is the same: the consumer has a way to say "not yet," and the producer has a way to hear it.

Two systems exchanging a feedback signal, one telling the other to slow down
javascript
const canContinue = writableStream.write(chunk);

if (!canContinue) {
    // The consumer just said "slow down" — respect it.
    await once(writableStream, "drain");
}

The failure mode isn't usually "we forgot backpressure exists." It's "we built a system where the fast part has no way to hear the slow part say no" — a fire-and-forget HTTP call to a webhook target, an unbounded Promise.all firing a thousand requests at once, a producer pushing onto a queue with no depth limit. There's no conversation happening. There's just a producer, alone, assuming the best.

Three Ways to Actually Implement It

A bounded queue filled to capacity, rejecting extra incoming items at the top

Bounded queues with rejection. Give the queue a maximum depth, and when it's full, reject new work instead of accepting it indefinitely. This turns an invisible, gradual failure (memory creeping up for six hours) into a visible, immediate one (a 429 or a rejected message right now) — which is a trade worth making, because immediate and visible is debuggable, and gradual and invisible is a postmortem.

Credit-based flow control. The consumer explicitly grants the producer permission to send N more items, and the producer stops when it runs out of credit. This is how HTTP/2 and gRPC manage streams under the hood, and it's the right model any time the consumer's actual capacity varies over time in a way the producer can't guess at from the outside.

Load shedding. When you're overwhelmed and can't apply backpressure upstream fast enough (a webhook sender you don't control, say), the fallback is to deliberately drop or degrade lower-priority work to protect the system as a whole. This is worse than true backpressure — you're losing information instead of just delaying it — but it beats an outage every time.

The Webhook Case, Specifically

A burst of incoming events funneling through a buffer and draining out as a slow, steady trickle

Webhooks make this concrete because you usually don't control the sender. If a partner's system fires 50,000 events at your endpoint in a burst, you can't ask them to add a receive window. What you can do is put a queue in front of your actual processing — accept the HTTP request fast, acknowledge it, and let your own consumer drain that queue at whatever rate your downstream systems can actually handle. The backpressure conversation still happens; it just happens between your ingestion layer and your processing layer instead of between you and a partner who was never going to slow down for you.

The Takeaway

Nobody puts "handle backpressure" on a feature roadmap, because it doesn't add a capability — it removes a failure mode that only shows up under load nobody tested for. That's exactly why it's worth building in on purpose, deliberately, before the day your producer finally outruns your consumer. The alternative isn't "we won't need it." The alternative is finding out you needed it from an incident channel.

FAQ

Isn't rate limiting the same thing as backpressure?

They're related but not identical. Rate limiting caps throughput based on a fixed policy regardless of current consumer capacity. Backpressure is a live feedback signal reflecting the consumer's actual, current ability to keep up, which can vary from one minute to the next.

How do I add backpressure to a fire-and-forget webhook sender I don't control?

You generally can't make the external sender respect backpressure directly. Instead, put a bounded queue in front of your own processing so you can absorb bursts, acknowledge quickly, and drain the queue at a rate your downstream systems can actually handle.

What's the risk of just making queues bigger instead of implementing backpressure?

A bigger queue delays the failure without preventing it, and it trades an immediate, visible error for a slower, harder-to-diagnose one — typically a memory or latency blowup once the queue finally does fill up, often at the worst possible time.

Does backpressure hurt throughput?

It can reduce peak throughput compared to accepting everything unconditionally, but that's the trade-off it's designed to make: slightly lower peak throughput in exchange for the system staying up and predictable under real-world load spikes.

We use cookies on this site to enhance your user experience

By clicking the Accept button, you agree to us doing so. More info on our cookie policy